package com.trgis.geotools;

import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.io.ParseException;
import com.vividsolutions.jts.io.WKTReader;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.geotools.data.*;
import org.geotools.data.collection.ListFeatureCollection;
import org.geotools.data.shapefile.ShapefileDataStore;
import org.geotools.data.shapefile.ShapefileDataStoreFactory;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.data.simple.SimpleFeatureIterator;
import org.geotools.data.simple.SimpleFeatureSource;
import org.geotools.data.simple.SimpleFeatureStore;
import org.geotools.feature.DefaultFeatureCollection;
import org.geotools.feature.SchemaException;
import org.geotools.feature.simple.SimpleFeatureBuilder;
import org.geotools.feature.simple.SimpleFeatureTypeBuilder;
import org.opengis.feature.Property;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.springframework.stereotype.Component;

import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.io.StringWriter;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.nio.charset.Charset;
import java.util.*;

/**
 * 2018/9/28
 * 默认读写shp文件使用的GBK编码，可使用setCharsetName方法指定编码
 * @author wangfy
 */
@Component
public class ShapeTools<T> {
    /**
     * shp文件定义字段时，字段名长度不能多于10个字符
     */
    private final int FEILD_LENGTH_LIMIT = 10;

    /**
     * shp文件定义字段时，空间属性字段必须是the_geom
     */
    private final String GEOM_FIELD_NAME = "the_geom";

    private String charsetName = "GBK";

    public void setCharsetName(String charsetName){
        if(null == charsetName || "".equals(charsetName.trim())){
            return;
        }
        this.charsetName = charsetName;
    }

    /**
     * 2017年11月23日
     * 读取shp文件，把名称和fid保存到ES中，
     * @author 王风雨
     * @param fileName
     * @param url 例： http://localhost:9200/dmdz2017/poi/_bulk </br>
     *            通过http  _bulk命令批量建索引的方式
     * @throws MalformedURLException
     * @throws IOException
     */
    public void readShpToES(String fileName, String url) throws MalformedURLException, IOException {
        ShapefileDataStoreFactory dataStoreFactory = new ShapefileDataStoreFactory();
        WKTReader reader = new WKTReader();
        File file = new File(fileName);
        ShapefileDataStore sds = (ShapefileDataStore) dataStoreFactory.createDataStore(file.toURI().toURL());
        sds.setCharset(Charset.forName(charsetName));
        List<Map<String, String>> fs = new ArrayList<>();
        SimpleFeatureSource featureSource = sds.getFeatureSource();
        SimpleFeatureIterator iterator = featureSource.getFeatures().features();
        Property per = null;
        while(iterator.hasNext()){
            SimpleFeature feature = iterator.next();
            Iterator<Property> it = feature.getProperties().iterator();
            Map<String, String> properties = new HashMap<>();
            while(it.hasNext()){
                per = it.next();
                String key = per.getName().toString();
                properties.put(key, per.getValue().toString());
            }
            fs.add(properties);
        }
        shpToES(fs, url);
    }

    /**
     * 文件名加上时间戳
     * @param fname
     * @return
     */
    private String rename(String fname){
        int index = fname.lastIndexOf(".");

        String temp = fname.substring(0, index) + System.currentTimeMillis();
        temp += fname.substring(index);

        return temp;
    }

    /**
     * 使用Apache http建立ES索引
     * @param fs
     * @param url http://localhost:9200/dmdz2017/poi/_bulk
     * @return
     * @throws IOException
     */
    private boolean shpToES(List<Map<String, String>> fs, String url) throws IOException{
        if(null == url || "".equals(url)) return false;
        long startTime = System.currentTimeMillis();
        PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
        CloseableHttpClient httpclient = HttpClients.custom().setConnectionManager(cm).build();
        HttpPost httpPost = new HttpPost(url);
        StringBuilder sb = new StringBuilder();
        //
        Map<String, String> prs = null;
        for(int i = 0, total = fs.size(); i < total; i++){
            prs = fs.get(i);
            sb.append("{\"create\":{\"_id\":\"").append(startTime + i).append("\"}}\n");
            sb.append(toJson(prs)).append("\n");
            //System.out.println(sb.toString());
            if((i % 200) == 0 || i == total-1){
                httpPost.setEntity(new StringEntity(sb.toString(), "utf-8"));
                CloseableHttpResponse response = httpclient.execute(httpPost);
                response.close();
                sb = null;
                sb = new StringBuilder();
                System.out.println("数据入库完成，总共"+total+"，已完成" + i + ", 耗时 " + (System.currentTimeMillis() - startTime) + "毫秒");
            }
        }
        httpclient.close();
        return true;
    }

    private String toJson(Map<String, String> ms){
        if(null == ms) {
            return "{}";
        }
        StringBuilder sb = new StringBuilder("{");
        for(Iterator<String> iterator = ms.keySet().iterator(); iterator.hasNext(); sb.append(",")){
            String k = iterator.next();
            sb.append("\"").append(k).append("\":\"")
                    .append(ms.get(k)).append("\"");
        }
        sb.deleteCharAt(sb.lastIndexOf(","));
        sb.append("}");
        return sb.toString();
    }


    /**
     * 一个测试创建shp的方法
     * @throws SchemaException
     * @throws ParseException
     * @throws IOException
     */
    @Deprecated
    protected void testCreateFeature() throws SchemaException, ParseException, IOException{
        final SimpleFeatureType featureType = DataUtilities.createType("Location",
                "the_geom:MultiPolygon:4490,"
                        + "name:String,"
                        + "type:String,"
                        + "typeCode:String");

        List<SimpleFeature> features = new ArrayList<>();

        SimpleFeatureBuilder sfb = new SimpleFeatureBuilder(featureType);
        WKTReader wktReader = new WKTReader();
        Geometry g = wktReader.read("MULTIPOLYGON(((120.245550628244 30.3697319771691,120.245604273926 30.3693135531255,120.24339413664 30.3690667936267,120.243630170808 30.368079739938,120.24205303449 30.3678007917758,120.241409305793 30.3668673845079,120.241291288517 30.3659447050454,120.241838457153 30.3652902449766,120.24261093291 30.3647216149009,120.242857696301 30.3633590530615,120.242986442154 30.3626831362105,120.246097798899 30.3664382241636,120.249713410056 30.3696568698573,120.249788512627 30.3709336004773,120.249482741414 30.3710677121942,120.247229688992 30.3708906898944,120.247353069806 30.3699948315325,120.245513078403 30.3698285368174,120.245550628244 30.3697319771691,120.245550628244 30.3697319771691)),((120.250083554427 30.3672053292808,120.250094283151 30.367993898842,120.249713410056 30.3696568698573,120.247953884385 30.3681011910327,120.24685954468 30.3670712245555,120.247661524439 30.3666179298771,120.248780002808 30.365907142485,120.249450554592 30.366513321681,120.250083554427 30.3672053292808,120.250083554427 30.3672053292808,120.250083554427 30.3672053292808)))");
        sfb.add(g);
        sfb.add("ming cheng");
        sfb.add("lei xing");
        sfb.add("type code");
        features.add(sfb.buildFeature("idididi"));
        ShapefileDataStoreFactory dataStoreFactory = new ShapefileDataStoreFactory();

        Map<String, Serializable> params = new HashMap<>();
        File f = new File("d:\\test.shp");
        params.put("url", f.toURI().toURL());
        params.put("create spatial index", Boolean.TRUE);

        ShapefileDataStore newDataStore = (ShapefileDataStore) dataStoreFactory.createNewDataStore(params);
        newDataStore.createSchema(featureType);
        String typeName = newDataStore.getTypeNames()[0];
        SimpleFeatureSource featureSource = newDataStore.getFeatureSource(typeName);

        SimpleFeatureStore featureStore = (SimpleFeatureStore) featureSource;

        SimpleFeatureCollection collection = new ListFeatureCollection(featureType, features);
        Transaction transaction = new DefaultTransaction("create");
        featureStore.setTransaction(transaction);
        featureStore.addFeatures(collection);
        transaction.commit();

    }

    /**
     * 指定创建shp文件的字段属性，注意字段名称长度不能超过10个字符</br>
     * 2018年11月2日，除了空间属性外，此方法只能生成全字符类型的其他属性
     * @param heads
     * @param geomName
     * @param geomClass
     * @return
     */
    @Deprecated
    public SimpleFeatureTypeBuilder createFeatureTypeBuilder(String[] heads, String geomName, Class geomClass){
        SimpleFeatureTypeBuilder sftb = new SimpleFeatureTypeBuilder();
        for(String name : heads){
            if(name.equals(geomName)){
                sftb.add(GEOM_FIELD_NAME, geomClass);
            } else {
                sftb.add(name, String.class);
            }
        }
        return sftb;
    }

    /**
     * 2018年10月29日
     * 如果是为了生成shp文件而调用此方法，注意字段名称长度不能超过10个字符
     * @param clazz 有com.trgis.geotools.GeoJSON注解的空间类
     * @return
     */
    public SimpleFeatureTypeBuilder createFeatureTypeBuilder(Class clazz){
        SimpleFeatureTypeBuilder sftb = new SimpleFeatureTypeBuilder();
        String name = ((GeoJSON)clazz.getAnnotation(GeoJSON.class)).name();
        if(null == name){
            sftb.setName("feature");
        } else {
            sftb.setName(name);
        }
        boolean hasGeometry = false;
        Field[] fs = clazz.getDeclaredFields();
        for(Field f : fs){
            GeoJSON g = f.getAnnotation(GeoJSON.class);
            if(null == g)
                continue;
            String field = g.field();
            if("".equals(field)){
                field = f.getName();
            }
            if(!hasGeometry && g.isGeometry()) {
                hasGeometry = true;
                field = GEOM_FIELD_NAME;
            }
            Class type = g.type();
            if(type == Object.class)
                sftb.add(field, f.getType());
            else
                sftb.add(field, type);
        }
        if(!hasGeometry)
            throw new NullPointerException("GeoJSON : annotation must has geometry field");
        return sftb;
    }

    /**
     * 将封装好的空间数据及属性写入到shp文件
     * @param tb
     * @param list 每个属性对应的数据对象，属性名称不能超过10个字符
     * @param filePath
     * @return
     * @throws IOException
     */
    public boolean writeToShpFile(SimpleFeatureTypeBuilder tb, List<Map<String, Object>> list, String filePath) throws Exception {
        boolean b = false;
        Map<String, Serializable> params = new HashMap<String, Serializable>();
        FileDataStoreFactorySpi factory = new ShapefileDataStoreFactory();
        params.put(ShapefileDataStoreFactory.URLP.key,
                new File(filePath).toURI().toURL());
        ShapefileDataStore ds = (ShapefileDataStore) factory.createNewDataStore(params);
        tb.setName("shapefile");
        SimpleFeatureType sft = tb.buildFeatureType();
        ds.createSchema(sft);
        ds.setCharset(Charset.forName(charsetName));
        FeatureWriter<SimpleFeatureType, SimpleFeature> writer = ds.getFeatureWriter(
                Transaction.AUTO_COMMIT);
        //list中对象写入feature对象
        for (int i = 0; i < list.size(); i++){
            SimpleFeature feature = writer.next();
            for(Map.Entry<String, Object> entry : list.get(i).entrySet()){
                if(entry.getKey().length() > FEILD_LENGTH_LIMIT) throw new Exception("定义shp文件的属性字段名不能超过10个字符：" + entry.getKey());
                if(null != feature.getProperty(entry.getKey()))
                    feature.setAttribute(entry.getKey(), entry.getValue());
            }
        }
        writer.write();
        writer.close();
        ds.dispose();
        b = true;
        return b;
    }

    /**
     * 将空间对象集合保存到shp文件，空间对象必须是有GeoJSON注解的
     * 2018年11月1日
     * @param objs
     * @param filePath
     * @return
     * @throws Exception
     * @author 王风雨
     */
    public boolean writeToShpFromGeoJSON (List<T> objs, String filePath) throws Exception {
        boolean b = false;
        if(objs.size() == 0)
            return b;
        Class clazz = objs.get(0).getClass();
        GeoJSON g = objs.get(0).getClass().getAnnotation(GeoJSON.class);
        if(null == g)
            return b;
        SimpleFeatureTypeBuilder simpleFeatureTypeBuilder = new SimpleFeatureTypeBuilder();
        simpleFeatureTypeBuilder.setName(g.name());
        boolean hasGeometry = false;
        Field[] fields = clazz.getDeclaredFields();
        Map<String, String> fieldMap = new HashMap<>();
        for(Field f : fields){
            g = f.getAnnotation(GeoJSON.class);
            if(null == g)
                continue;
            String field = g.field();
            if("".equals(field)){
                field = f.getName();
            }
            if(field.length() > FEILD_LENGTH_LIMIT)
                throw new Exception("定义shp文件的属性字段名不能超过10个字符" + field);
            if(!hasGeometry && g.isGeometry()) {
                hasGeometry = true;
                field = GEOM_FIELD_NAME;
            }
            Class type = g.type();
            if(type == Object.class)
                simpleFeatureTypeBuilder.add(field, f.getType());
            else
                simpleFeatureTypeBuilder.add(field, type);

            fieldMap.put(field, f.getName());
        }
        if(!hasGeometry)
            throw new NullPointerException("GeoJSON : annotation must has geometry field");
        List<Map<String, Object>> list = new ArrayList<>();
        for(T t : objs){
            Map<String, Object> obj = new HashMap<>();
            for(Map.Entry<String, String> fieldEntry : fieldMap.entrySet()){
                Method method = clazz.getMethod(getMethodName(fieldEntry.getValue()));
                obj.put(fieldEntry.getKey(), method.invoke(t));
            }
            list.add(obj);
        }
        b = writeToShpFile(simpleFeatureTypeBuilder, list, filePath);
        return b;
    }

    /**
     * 读取shp文件，返回对象集合
     * @param filePath 文件名及全路径
     * @param shpEntityBuilder feature对象转换成指定对象的接口
     * @return
     * @throws Exception
     */
    public List<T> readShp(String filePath, ShpEntityBuilder<T> shpEntityBuilder) throws Exception {
        ShapefileDataStoreFactory dataStoreFactory = new ShapefileDataStoreFactory();
        File file = new File(filePath);
        ShapefileDataStore sds = (ShapefileDataStore) dataStoreFactory.createDataStore(file.toURI().toURL());
        sds.setCharset(Charset.forName(charsetName));
        SimpleFeatureIterator iterator = sds.getFeatureSource().getFeatures().features();
        List<T> datas = new ArrayList<>();
        while (iterator.hasNext()){
            T data = shpEntityBuilder.createEntity(iterator.next());
            if(null != data)
                datas.add(data);
        }
        return datas;
    }

    /**
     * 将有GeoJSON注解的空间对象转换成SimpleFeature
     * @param obj 只针对有GeoJSON注解的字段
     * @return
     * @throws IOException
     * @throws InvocationTargetException
     * @throws IllegalAccessException
     * @throws NoSuchMethodException
     */
    public SimpleFeature toSimpleFeature(Object obj) throws IOException, InvocationTargetException, IllegalAccessException, NoSuchMethodException {
        Class clazz = obj.getClass();
        SimpleFeatureTypeBuilder simpleFeatureTypeBuilder = createFeatureTypeBuilder(clazz);
        SimpleFeatureBuilder simpleFeatureBuilder = new SimpleFeatureBuilder(simpleFeatureTypeBuilder.buildFeatureType());
        String id = null;
        Map<String, Object> values = new HashMap<>();
        Field[] fields = clazz.getDeclaredFields();
        for(Field field : fields){
            GeoJSON g = field.getAnnotation(GeoJSON.class);
            if(null == g)
                continue;
            if(g.isID()){
                Method method = clazz.getMethod(getMethodName(field.getName()));
                id = method.invoke(obj).toString();
            } else {
                String fieldName =  g.field();
                if("".equals(fieldName)){
                    fieldName = field.getName();
                }
                if(g.isGeometry()){
                    fieldName = GEOM_FIELD_NAME;
                }
                Method method = clazz.getMethod(getMethodName(field.getName()));
                values.put(fieldName, method.invoke(obj));
            }
        }
        if(null == id)
            id = String.valueOf(System.currentTimeMillis());
        SimpleFeature feature = simpleFeatureBuilder.buildFeature(id);
        for (Map.Entry<String, Object> entry : values.entrySet()){
            feature.setAttribute(entry.getKey(), entry.getValue());
        }
        return feature;
    }

    /**
     * 将空间类集合转换为FeatureCollection对象
     * @param list 必须是有GeoJSON注解的空间类对象集合
     * @return
     * @throws InvocationTargetException
     * @throws NoSuchMethodException
     * @throws IllegalAccessException
     * @throws IOException
     */
    public SimpleFeatureCollection toSimpleFeatureCollection(List<Object> list) throws InvocationTargetException, NoSuchMethodException, IllegalAccessException, IOException {
        DefaultFeatureCollection featureCollection = new DefaultFeatureCollection();
        for(Object obj : list){
            featureCollection.add(toSimpleFeature(obj));
        }
        return featureCollection;
    }

    /**
     * 将有GeoJSON注解的空间对象转换成GeoJSON格式字符串
     * @param obj 只针对有GeoJSON注解的字段
     * @return
     * @throws IOException
     * @throws InvocationTargetException
     * @throws IllegalAccessException
     * @throws NoSuchMethodException
     */
    public String toGeoJSON(Object obj) throws IOException, InvocationTargetException, IllegalAccessException, NoSuchMethodException {
        SimpleFeature feature = toSimpleFeature(obj);
        StringWriter writer = new StringWriter();
        org.geotools.geojson.GeoJSON.write(feature, writer);
        return writer.toString();
    }

    private String getMethodName(String field){
        char first = field.charAt(0);
        String str = String.valueOf(first).toUpperCase();
        String name = field.replaceFirst(String.valueOf(first), str);
        return "get" + name;
    }

    /**
     * 由Feature对象生成指定对象的接口，get Feature对象的属性值 set 到指定对象，前提是知道属性名称和数据类型的对应关系
     * 例如 poi.setName(feature.getAttribute("name"))
     * @param <T>
     */
    public interface ShpEntityBuilder<T> {
        /**
         * 根据字段对应关系，生成指定对象
         * @param feature
         * @return
         * @throws Exception
         */
        T createEntity(SimpleFeature feature) throws Exception;
    }

    private  String getSetMethodName(String field){
        char first = field.charAt(0);
        String str = String.valueOf(first).toUpperCase();
        String name = field.replaceFirst(String.valueOf(first), str);
        return "set" + name;
    }

    /**
     * 2018年11月2日
     * 传递GeoJSON注释的类，使用注解的对应关系将shp文件读取的数据转换成指定对象
     * @param clazz
     * @return
     */
    public ShpEntityBuilder<T> useDefaultBuilder(Class clazz) {
        return feature -> {
            GeoJSON g = (GeoJSON) clazz.getAnnotation(GeoJSON.class);
            if(null == g)
                throw new Exception("非GeoJSON注解的类");
            T t = (T)clazz.newInstance();
            Field[] fields = clazz.getDeclaredFields();
            for(Field f : fields){
                g = f.getAnnotation(GeoJSON.class);
                if(null == g)
                    continue;
                Method method = clazz.getMethod(getSetMethodName(f.getName()), f.getType());
                // 当对象id的注解上没指定到feature的属性时
                // 因feature的属性中一般不包括id，但读取时可以用feature.getID(),给对象的id赋值
                if(g.isID() && "".equals(g.field())){
                    method.invoke(t, feature.getID());
                } else {
                    method.invoke(t, feature.getAttribute(g.isGeometry() ? GEOM_FIELD_NAME : "".equals(g.field()) ? f.getName() : g.field()));
                }
            }
            return t;
        };
    }
}
